/*
 * pixiv_down - CLI-based downloading tool for https://www.pixiv.net.
 * Copyright (C) 2023, 2024, 2025  Mio
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
module app.app;

import std.datetime.systime : SysTime;

import pd.configuration;
import std.experimental.logger;
import app.config;
import app.vcs_tag;

version(PD_NO_CONVERTER) {
   enum platformSpecificConverter = null;
} else version(OSX) {
   enum platformSpecificConverter = "imageio";
} else version(Posix) {
   enum platformSpecificConverter = "gm";
} else {
   // Don't static assert, since non-ugoira content could still be downloaded.
   enum platformSpecificConverter = null;
}

/*
 * --------------------------------------------------------------------------
 *                                 Handles
 * --------------------------------------------------------------------------
 */

///
/// Handle any "help" commands.
///
/// This only runs when "help" is used as a command by itself, or when it
/// is succeeded by a different command (thus displaying the help message
/// for that command).
///
int helpHandle(string[] args, const ref Config)
{
   import app.cmds;

   if (args.length == 1) {
      displayDefaultHelp(true);
      return 0;
   }

   switch(args[1]) {
      case "artist":
         displayArtistHelp();
         break;
      case "artwork":
         displayArtworkHelp();
         break;
      case "bookmarked":
         displayBookmarkedHelp();
         break;
      case "compact":
         displayCompactHelp();
         break;
      case "daily":
         displayDailyHelp();
         break;
      case "errors":
	 displayErrorsHelp();
	 break;
      case "following":
         displayFollowingHelp();
         break;
      case "novel":
         displayNovelHelp();
         break;
      case "prune":
         displayPruneHelp();
         break;
      default:
         displayDefaultHelp();
         return 2;
   }

   return 0;
}


/*
 * --------------------------------------------------------------------------
 *                                  Help
 * --------------------------------------------------------------------------
 */

///
/// Print the default help information.
///
/// Params:
///  showMore    = Show examples and links to pixiv_down's websites.
///
void displayDefaultHelp(bool showMore = false)
{
   import std.stdio : stderr;

   stderr.writefln(
      "pixiv_down - A tool for downloading from pixiv.net [%s (%s)]\n" ~
      "\nUsage:\tpixiv_down <command> [options]\n" ~
      "\npixiv_down is a tool that allows you to download various content\n" ~
      "from the pixiv website. You can download illustrations, manga, and\n" ~
      "novels.\n" ~
      "\nMain Commands:\n" ~
      "\tartist    \tDownload content from a specified artist.\n" ~
      "\tartwork   \tDownload specific artworks via IDs.\n" ~
      "\tbookmarked\tDownload all bookmarked content.\n" ~
      "\tcompact   \tCompact all account directories in to one.\n" ~
      "\tdaily     \tDownload all followed users' content between dates.\n" ~
      "\terrors    \tPrint the work ID of all works that have failed to download.\n" ~
      "\tfollowing \tDownload all followed users' content by account.\n" ~
      "\tnovel     \tDownload specific novels via IDs.\n" ~
      "\tprune     \tRemove unfollowed and/or non-existing accounts.\n" ~
      "\nSide Commands:\n" ~
      "\thelp      \tDisplay this help information and exit.\n" ~
      "\treset     \tReset your PHPSESSID.\n" ~
      "\tversion   \tDisplay the current version of pixiv_down.",
      PROJECT_VERSION_STRING, VCS_TAG);

   if (showMore) {
      stderr.writeln(
         "\nExamples:\n" ~
         "\n  Download a single illustration:\n" ~
         "      pixiv_down artwork 108985926\n" ~
         "\n  Download all content from a single user:\n" ~
         "      pixiv_down artist 10109777\n" ~
         "\nYou can find more information about pixiv_down at\n" ~
         "  <https://yume-neru.neocities.org/p/pixiv_down.html>.\n" ~
         "\nIf you have any issues or feedback, please see the project's page" ~
         " at codeberg\n" ~ "  <https://codeberg.org/supercell/pixiv_down>.");
   } else {
      stderr.writeln("\n" ~
         "For more information (including examples) use the ``help'' command.");
   }
}

/*
 * --------------------------------------------------------------------------
 *                                  Util
 * --------------------------------------------------------------------------
 */

/// Returns the two character locale set in the environment.
string getLocale()
{
   import core.stdc.locale;
   import std.string : fromStringz;

   setlocale(LC_ALL, "");
   char* currentLocale = setlocale(LC_MESSAGES, null);
   if (null is currentLocale) {
      return "en";
   }
   char[] arr = fromStringz(currentLocale);
   if ("C" == arr || "POSIX" == arr) {
      return "en";
   }

   return arr[0..2].dup;
}

/*
 * --------------------------------------------------------------------------
 *                                  Main
 * --------------------------------------------------------------------------
 */
enum RunCommandStatus
{
   ok,
   subCommandFailed,
   invalidCommand
}

RunCommandStatus runCommand(string command, string[] args, const ref Config config)
{
   import app.cmds;

   int res = 0;

   switch (command) {
   case "artist":
      res = artistHandle(args, config);
      break;
   case "artwork":
      res = artworkHandle(args, config);
      break;
   case "bookmarked":
      res = bookmarkedHandle(args, config);
      break;
   case "compact":
      res = compactHandle(args, config);
      break;
   case "daily":
      res = dailyHandle(args, config);
      break;
   case "errors":
      res = errorsHandle(args, config);
      break;
   case "following":
      res = followingHandle(args, config);
      break;
   case "novel":
      res = novelHandle(args, config);
      break;
   case "prune":
      res = pruneHandle(args, config);
      break;
   case "help":
      res = helpHandle(args, config);
      break;
   default:
      return RunCommandStatus.invalidCommand;
   }

   return (res == 0) ? RunCommandStatus.ok : RunCommandStatus.subCommandFailed;
}

int main(string[] args)
{
   import app.logger : initializeLogger;

   import std.stdio : stderr, writefln;
   import std.string : empty;

   import pd.converter;

   if (args.length < 2) {
      displayDefaultHelp(false);
      return 2;
   }

   Config cliConfigOverrides;
   string configFilePathOverride;

   // skip the vaue in --option value.
   bool skipArg = false;
   ptrdiff_t subCommandIndex = -1;

   foreach(idx, arg; args[1..$]) {
      import app.util: split;
      import std.string: indexOf, startsWith;

      if (skipArg) {
         skipArg = false;
         continue;
      }

      if (!arg.startsWith("-")) {
         subCommandIndex = idx + 1;
         break;
      }

      switch(arg) {
      case "-h":
      case "--help":
         displayDefaultHelp(true);
         return 0;
      case "-v":
      case "--version":
      case "version":
         writefln("pixiv_down %s (%s)", PROJECT_VERSION_STRING, VCS_TAG);
         return 0;
      case "--output-directory":
         if (idx + 2 >= args.length) {
            stderr.writefln("error: invalid argument: %s (no output directory provided)", arg);
            return 1;
         }
         cliConfigOverrides.outputDirectory = args[idx + 2];
         skipArg = true;
         break;
      case "--config-file":
         if (idx + 2 >= args.length) {
            stderr.writefln("error: invalid argument: %s (no path provided for config file", arg);
            return 1;
         }
         configFilePathOverride = args[idx + 2];
         skipArg = true;
         break;
      case "--converter":
         if (idx + 2 >= args.length) {
            stderr.writefln("error: invalid argument: %s (no converter provided)", arg);
            return 1;
         }
         cliConfigOverrides.converterName = args[idx + 2];
         skipArg = true;
         break;
      case "reset":
         resetSessionID();
         return 0;
      default:
         break;
      }

      if (arg.startsWith("--") && arg.indexOf("=") != -1) {
         string[] argAndValue = arg.split('=', 2);
         switch (argAndValue[0]) {
         case "--config-file":
            configFilePathOverride = argAndValue[1];
            break;
         case "--output-directory":
            cliConfigOverrides.outputDirectory = argAndValue[1];
            break;
         case "--converter":
            cliConfigOverrides.converterName = argAndValue[1];
            break;
         default:
            break;
         }
      }
   }

   if (subCommandIndex < 0) {
      stderr.writefln(`%s: no command provided. See "%s help".`, args[0], args[0]);
      return 1;
   }

   initializeLogger();

   Config config = empty(configFilePathOverride) ? loadConfig() : loadConfig(configFilePathOverride);
   if (!empty(cliConfigOverrides.outputDirectory))
   {
      infof("Provided config outputDirectory override: %s", cliConfigOverrides.outputDirectory);
      config.outputDirectory = cliConfigOverrides.outputDirectory;
   }
   infof("config outputDirectory: %s", config.outputDirectory);

   // Attempt to use override followed by configuration file to determine Converter Plugin.
   if (!empty(cliConfigOverrides.converterName))
   {
      config.manager = ConverterManager.load(cliConfigOverrides.converterName);
      infof("converterName override (%s) loaded? %s",
            cliConfigOverrides.converterName, config.manager ? "true" : "false");
   }
   if (!config.manager && !empty(config.converterName))
   {
      config.manager = ConverterManager.load(config.converterName);
      infof("converterName config (%s) loaded? %s", config.converterName,
            config.manager ? "true" : "false");
   }
   if (!config.manager && (config.converterName != platformSpecificConverter))
   {
      config.manager = ConverterManager.load(platformSpecificConverter);
      infof("platformSpecificConverter (%s) loaded? %s", platformSpecificConverter,
            config.manager ? "true" : "false");
   }

   if (!config.manager) {
      stderr.writeln("No converter library found. Ugoira will not download.");
   } else if (!config.manager.initialize()) {
      // Override, config, and platform-specific converter failed.
      stderr.writefln("Failed to load the '%s' converter. Ugoira will not download.",
            config.converterName);
   }
   else
   {
      infof("loaded converter: %s (%s)", config.manager.name(), config.converterName);
   }

   scope (exit)
   {
      if (config.manager)
         config.manager.deinitialize();
   }

   config.locale = getLocale();
   infof("Current locale = %s", config.locale);

   RunCommandStatus status = runCommand(args[subCommandIndex], args[subCommandIndex..$], config);
   if (status == RunCommandStatus.invalidCommand) {
      stderr.writefln(`%s: "%s" is not a valid pixiv_down command. See "%s help".`, args[0],
                      args[subCommandIndex], args[0]);
      return 1;
   }

   return (status == RunCommandStatus.ok) ? 0 : 1;
}
